今天我們預計會完成 登入/註冊的頁面撰寫,明天我們再將實際的功能串上。樣式是出自於我們的設計稿。

今天會是超級大工程,篇幅也會比較長 XDD 不過今天產出的結果也同樣會很可觀喔!
CupertinoApp 建構子可以用於建構 iOS style 的應用程式,其中提供了許多包括設定主題、首頁、路由、語系等等的參數可供使用。有興趣的可以用我們之前教的方式來檢視該類別的定義,以下我們提供簡單的範例並請將程式碼 MyApp 中的 build 函式改為此,並嘗試閱讀看看:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'My Cupertino App',
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Home'),
),
child: Center(
child: Text('Welcome to Cupertino App!'),
),
),
);
}
}
從上例我們可以得到幾個資訊:
CupertinoPageScaffold:用於建立 Cupertino 風格的頁面佈局(骨架),通常應用程式單個頁面中,可能會有
navigatin bar 頂部欄位用於標示現在位置或回到上一頁的操作,可使用 CupertinoNavigationBar 來建構child 主要顯示內容區域,因此主要內容呈現會寫在這裡我們首先來實作登入頁面,因為登入頁面有輸入框,可以將輸入框理解成無論有任何輸入文字、刪除文字的動作都會需要觸發一次更新來更新目前輸入框中的文字內容。
為了完成上述動作,因此需要使用到 state 來記錄當前輸入的內容,因此請在 main.dart 中空白區域建立一個 LoginScreen 的 stateful widget,並將 CupertinoApp 中 home 的參數改成 LoginScreen() ,使得應用程式初始的畫面為此登入畫面。
在 Cupertino 的 widgets 中,有一個 widget 為 CupertinoTextField 可以作為輸入框的工具來使用。
CupertinoTextField 中有一個參數為 controller 型態為 TextEditingController ,無論使用者對出輸入框進行什麼操作都與此 controller 有關。讓我們來看看怎麼使用吧~
class _LoginScreenState extends Stat<LoginScreen> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build (BuildContext context) {
return CupertinoPageScaffold(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CupertinoTextField(
controller: _controller,
placeholder: '尚未輸入文字之前,出現的提示字',
),
CupertinoButton(
onPressed: () {
print(_controller.text);
},
child: Text('登入'),
)
]
),
)))
}
}
如此每次點按「登入」按鈕時,我們就可以顯示我們在輸入框中輸入的內容。
由於登入畫面需要有兩個輸入框,一個是信箱、另一則是密碼,因此需要改寫一下上方的程式碼,這裡留給讀者練習,我們最後會分享今天的程式碼成果。
運用我們現有知識將當前畫面盡可能的符合設計圖樣式
Text),並調整字體大小、顏色與粗細CupertinoButton 同樣也進行樣式的調整提示:
- 一個
controller只能一次給一個輸入框使用- 使用
CupertinoTextField中的decoration參數與padding參數進行調整SizedBox可用於創造特定高度或寬度的空間作為間隔
練習 1 你將會花些時間反覆的進行文件的查詢,也可能會撞牆很多次... 不過這些時間都很值得!加油 💪
在登入頁面的最上方,我們有將一個鎖的圖片放在紅色的方形外框中,我們講述如何實作。
首先是外框,實作的方式有很多。而我使用的是 Container 這個 widget,為一個很常用的小工具,可以輕易的構建出四邊形的外框,並調整內外間距、背景顏色、邊框樣式等等。用 HTML 的 tag 來說,其實就很像 div 的存在。
以下提供簡單的 Container 範例:
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 30, 84, 1), // 設定 container 顏色
borderRadius: BorderRadius.all(Radius.circular(10)), // container 樣式為 10 單位的圓角
),
child: const Text('Container 內的文字')
)
上面的程式碼執行結果如下:

因此當我們今天將 Container 中的 child 從文字改為圖片就可以達成我們設計稿的效果了。請先建立一個 assets 資料夾於專案目錄中,裡面再建立 images 資料夾。此時你的專案目錄會像這樣
micro_news_tutorial/
├── 上面省略
├── assets/ -> 看個人喜好,我習慣與 lib 是同個層級的
├── fonts/ -> 放置字體 (可以一併建立)
└── images/ -> 放置圖片
├── lib/
└── 下面省略
接下來就是在網路上分別找到鎖、Google icon 與 Apple icon,並放置於 images 資料夾中。接下來請開啟 pubspec.yaml 檔案,並添加以下內容(順序不影響結果)
assets:
- assets/images/padlock.png
- assets/images/google.png
- assets/images/apple.png
這裡您可以視下載後的圖片檔名來進行修改。做以上這段的意義在於告訴 Flutter 要將 assets 資源加載至應用程式中,並於構建應用程式的過程 Flutter 會將資源放置到一個特殊的檔案 asset bundle 中,並從中來讀取資源。
所以同理,假設今天要加載字體檔案時,也要做同樣的步驟將字體資源載入至 asset bundle,並於文字顯示套用該字體時從中讀取所需資源。
現在我們就可以將 Text 的部分替換成圖片來進行顯示。此時可以使用 Image.asset 來加載應用程式中資源文件中的圖片 (我們剛剛已經放進 assets/images 中拉),我們來看看範例,並執行看看結果:
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 30, 84, 1), // 設定 container 顏色
borderRadius: BorderRadius.all(
Radius.circular(10)), // container 樣式為 10 單位的圓角
),
child: Image.asset('assets/images/padlock.png', height: 30) // 加載本地圖片文件,並設定高度為 30
)
讚的咧~~

在我們設計稿的最底部我們有 Google 與 Apple 的圖片,目的是讓使用者可以透過 Google 或 Apple 帳號快速登入而不必透過註冊流程,因此我們先把按鈕做出來。
有了上面的講解後,相信你已經可以很輕鬆的自己搭配 Container 與 Image.asset 的使用來造出相應的結果。
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.all(
Radius.circular(10),
),
border: Border.fromBorderSide(
BorderSide(color: CupertinoColors.systemGrey5))),
child: Image.asset('assets/images/apple.png', height: 30)),
const SizedBox(width: 24), // 讓兩個 Container 有些間隔
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.all(Radius.circular(10)),
border: Border.fromBorderSide(
BorderSide(color: CupertinoColors.systemGrey5)),
),
child: Image.asset('assets/images/google.png', height: 30)),
]),
不過我們還缺少了最關鍵的部分,也就是這兩者皆是「按鈕」,必須要「點擊」才會觸發特定的行為,但是 Container 並沒有提供讓我們觸發事件的參數。讓我們從目前學到的 Cupertino 相關的 widget,哪個是可以進行點按的呢?
沒錯!就是 CupertinoButton,讓我們把 Container 當成它的 child 傳進去就可以透過使用 CupertinoButton 的 onPressed 參數來觸發點擊事件拉。
CupertinoButton(
onPressed: () {
print('Google 登入');
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.all(Radius.circular(10)),
border: Border.fromBorderSide(
BorderSide(color: CupertinoColors.systemGrey5)),
),
child: Image.asset('assets/images/google.png', height: 30)),
)
並且 CupertinoButton 因為實作了點擊的動畫,因此每當點擊 Google 或是 Apple 按鈕時都會有點擊的特效。

到這邊我們已經具備了所有可以構建出畫面的能力了,所以盡可能的完成「練習 1」 的挑戰吧!
我們先觀察註冊頁面,除了顯示的訊息與登入頁面有些不同、新增了一個輸入框,並且移除了下方的第三方登入按鈕外,其餘幾乎相同。因此這部分相信各位也有辦法獨立完成了!
讓我們在 main.dart 空白處建立一個 RegisterScreen 的 stateful widget。因為同樣也有輸入框,所以我們需要有 TextEditingController 的 state 來記錄輸入框的更動。並將 CupertinoApp 中 home 的參數改成 RegisterScreen(),使得應用程式初始的畫面為此註冊畫面。
其實登入頁面與註冊頁面的流程應為,當使用者有帳號時透過輸入帳號、密碼並點擊「登入」按鈕;若為否,則點擊「註冊帳號」跳轉至註冊頁面。兩頁面間其實應該要有跳轉機制,這部分後面章節會再提到,目前可以先忽略不計並暫時透過替換 CupertinoApp 的 home 參數來對個別頁面進行開發!
main.dart目前執行起來一切都很好,但是 main.dart 有點太大了... 我們說過 main.dart 中的 main 函式是整個應用程式的入口。所以我們應該要保持 main.dart 專門做一件事情就好,也就是負責開始運行我們的應用程式,其餘不相干的內容我們一律從 main.dart 中拆分出去。
這麼做的好處是很直接的,當我們應用程式一大起來,可能有數十頁的頁面需要設計,總不可能全數塞在 main.dart 中,從幾千幾萬行的程式碼中找到你想要修改的部分。因此適當的拆檔是讓你的程式碼可讀性、可維護性直線上升的技巧。
請在 lib 資料夾底下建立 views 資料夾,並於 views 底下再建立:
screens :存放顯示畫面的檔案 (登入畫面、註冊畫面等)widgets :存放可重複使用的元件(按鈕、新聞卡片等)並於 screens 資料夾中分別建立 login_screen.dart 與 register_screen.dart 兩個檔案,再分別將 LoginScreen 與 RegisterScreen 兩個 stateful widget 放置於各自檔案。
接下來便是將兩個檔案進行引入,引入方式如下
import 'package:flutter/cupertino.dart';
// 也可以寫作 import 'package:micro_news_tutorial/views/screens/login_screen.dart';
// 引用邏輯與引用外部套件相同,不過因為是引用自己目錄底下的檔案,因此前綴可以省略
import 'views/screens/login_screen.dart';
import 'views/screens/register_screen.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(home: LoginScreen());
}
}
這麼一來,經過方才簡單的動作 main.dart 變得乾淨了許多。往後我們也會適時的拆分 widget 讓程式碼更加的乾淨。
今天我們從 CupertinoApp 開始建立了一個 iOS 風格的應用程式,並使用了:
CupertinoTextField - 作為輸入框,需要有一個 controller 來監聽輸入框的內容是否更動CupertinoButton - 作為按鈕,使其中的 child 可以被觸發 onPressed 事件,以及點擊的特效Image.asset - 引入應用程式本地的圖片檔案Container - 作為圖片的外框透過以上的 Widgets 我們就完成了登入以及註冊畫面的製作,並為了維護程式碼的可讀性與可維護性,我們針對檔案的用途、頁面進行檔案的拆分,未來我們也將持續這麼做!
希望今天的內容能夠讓大家有所收穫~明天我們將實際的實現註冊與登入的功能!敬請期待。
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day11/micro_news_app